/** @define {boolean}*/
var DESIGNER = true;

//VisControl
//object creation
var _object;
var _registerProto;
var _getProtoDecl;
var ObjectCtrl;
var ProtoImpl;

var NewId;
var IdObj = function(){
	var _id=0;
	NewId = function(){
		return _id++;
	}
	/** @constructor */
	return function(){
		this._id = NewId();

		var cleanup = {};//for destructor!
		var dest_id=0;
		this.addDest = function(dest){
			dest_id++;
			cleanup[dest_id] = dest;
			return dest_id;
		}
		this.removeDest = function(dest_id){
			delete cleanup[dest_id];
		}
		this.destroy = function(){
			for (var k in cleanup){
				cleanup[k]();
			}
		}
	}
}();

/** StaticSocket is used as input
*/
var StaticSocket = function(defVal){
	IdObj.call(this);
	var v = defVal;
	this.set = function(val){
		v = val;
	}
	this.get = function(){
		return v;
	}
}

//TODO: unify all type of sockets into one single function
var SignalSocket = function(){
	IdObj.call(this);
	var self = this;
	var outputs={};
	var outCount = 0;
	this.set = function(){//signal can receive any type of pulse!
		//trigger all outputs
		for (var output in outputs){
			outputs[output].set();
		}
	}
	//delete this.get;//--> signal has no GETTER!
	var addOutput = function(receiver){
		outputs[receiver._id] = receiver;
		outCount ++;
		cleanups[receiver._id] = receiver.addDest(function(){
			self.removeOutput(receiver);
		});
	}
	var cleanups = {};
	this.addOutput = function(receiver){
		outputs[receiver._id] = receiver;
		outCount ++;
		cleanups[receiver._id] = receiver.addDest(function(){
			self.removeOutput(receiver);
		});
	}
	this.removeOutput = function(receiver)
	{
		delete outputs[receiver._id];
		outCount--;
		receiver.removeDest(cleanups[receiver._id]);
	}
}

/** @constructor */
var Socket = function(defVal, compare, no_set_at_connect){
	IdObj.call(this);
	var v = defVal;
	var outputs={};
	var outCount = 0;
	if (compare){
		this.set = function(val){
			if (compare(v,val)) return;
			v = val;
			//trigger all outputs
			for (var output in outputs){
				outputs[output].set(val);
			}
		}
	}else
		this.set = function(val){
			if (v==val) return;
			v = val;
			//trigger all outputs
			for (var output in outputs){
				outputs[output].set(val);
			}
		}
	this.get = function(){
		return v;
	}
	var self = this;
	var cleanups = {};
	var addOutput = function(receiver){
		outputs[receiver._id] = receiver;
		outCount ++;
		cleanups[receiver._id] = receiver.addDest(function(){
			self.removeOutput(receiver);
		});
	}
	if (no_set_at_connect)
		this.addOutput = addOutput;
	else
		this.addOutput = function(receiver){
			addOutput(receiver);
			receiver.set(v);
		}
	this.removeOutput = function(receiver)
	{
		delete outputs[receiver._id];
		outCount--;
		receiver.removeDest(cleanups[receiver._id]);
	}
	this._outCount = function(){
		return outCount;
	}
}


//forward declaration
var _registerClass;
var _create;
var _registerCtrl;
var _getCtrlDecl;
var BroadcastImpl;
var _getBroadcast;
var ScalarCtrlDesigner;

var CtrlDesignerAddSocket;
var CtrlDesigner;
var Menu;

(function(){
	var classes = {};
	_registerClass = function(name, constructor){
		classes[name] = constructor;
	};
	_create = function(name,params){
		var obj = Object.create(classes[name].prototype);
		/*
		var params = [];
		for (var i=1;i<arguments.length;i++){
			params.append(arguments[i]);
		}*/
		classes[name].apply(obj,params);
		IdObj.call(obj);

		//add to broadcast
		var br = _getBroadcast(name);
		if (br) br.add(obj);

		return obj;
	}
	var decls = {};
	var impls = {};
	_registerCtrl = function(name, decl, impl){
		decls[name] = decl;
		impls[name] = impl;
	}
	_getCtrlDecl = function(name){
		return decls[name];
	}
	_getCtrlImpl = function(name){
		return impls[name];
	}

})();

/** @constructor */
var _ctrlImpl = function(name, activeSockets){//create ctrlImpl
	var self = this;
	if (name in _scalarCtrl){
		_scalarCtrl[name].call(this);
		//ScalarCtrlDesigner(name, this);
	}
	else
		Socket.call(this);
	//create all sockets
	var sockets = {};
	this.sockets = sockets;
	var decl = _getCtrlDecl(name);
	var addEmptySocket = function(socket){
		if (decl[socket] instanceof Object){//should be a chip
			sockets[socket] = {};//to contain the chip's socket
		}else{//string --> scalar
			sockets[socket] = new _scalarCtrl[decl[socket]]();
			if (DESIGNER){//allow delete slot in DESIGNER mode
				sockets[socket].addDest(function(){
					delete sockets[socket];
				});
			}
		}
	}
	activeSockets.forEach(addEmptySocket);
	//if (DESIGNER){
		var rconnections = {};
	//}
	if (!(name in _scalarCtrl)){
		this.staticConnect = function(receiver){//share the sockets with receiver
			var ctrl = receiver.ctrls[name];
			for (var socket in sockets){
				if (ctrl[socket]) sockets[socket] = ctrl[socket];
				//redirect the designer here!
			}
		}
		//TODO: rename this to 'addOutput' and add removeOutput --> disconnect!
		//var outputs = {};
		this.connect = function(receiver){//forward value to receiver's sockets
			//outputs[receiver._id] = receiver;
			//connect all active slots
			var ctrl = receiver.ctrls[name];
			for (var socket in sockets){
				if (ctrl[socket]){
					sockets[socket].addOutput(ctrl[socket]);
				}
			}
		}
		var oldSet = this.set;
		this.set = function(val){
			var oldVal = self.get();
			if (oldVal != val){//this comparison is only valid for non-scalar
				oldSet(val);
				if (oldVal) self.disconnectReverse(oldVal);
				if (val) self.connectReverse(val);
			}
		}
		/*
		this.addOutput = function(objectSocket){
			self.connect(objectSocket.get());
		}//*/
		this.disconnect = function(receiver){
			//disconnect all active slots
			var ctrl = receiver.ctrls[name];
			for (var socket in sockets){
				if (ctrl[socket]) sockets[socket].removeOutput(ctrl[socket]);
				//TODO: add the opposite direction
			}
		}
		/*
		this.removeOutput = function(objectSocket){
			self.disconnect(objectSocket.get());
		}//*/
		this.connectReverse = function(sender){
			if (DESIGNER){
				rconnections[sender._id] = sender;
			}
			//connect all active slots
			var ctrl = sender.ctrls[name];
			for (var socket in sockets){
				if (ctrl[socket]){
					ctrl[socket].addOutput(sockets[socket]);//from object to controller first
				}
			}
		}
		this.disconnectReverse = function(sender){
			if (DESIGNER){
				rconnections[sender._id] = sender;
			}
			//connect all active slots
			var ctrl = sender.ctrls[name];
			for (var socket in sockets){
				if (ctrl[socket]){
					ctrl[socket].removeOutput(sockets[socket]);//from object to controller first
				}
			}
		}//*/
		this.groupConnect = function(groupCtrl){
			//connect to existing objects
			for (var key in groupCtrl.getObjects()){
				self.connect(groupCtrl.getObjects()[key]);
			}
			//connect to future objects
			var listener = new IdObj();
			listener.onAdd = function(obj){
				self.addOutput(obj);
			}
			listener.onRemove = function(obj){
				self.removeOutput(obj);
			}
			groupCtrl.addListener(listener);
		}
	}
	var impl = _getCtrlImpl(name);
	impl.call(this, activeSockets);//for on-demand output sockets
	if (DESIGNER){
		var div = CtrlDesigner(name, decl, self, activeSockets);
		this.addSocket = function(socket){
			addEmptySocket(socket);
			impl.call(self, [socket]);
			CtrlDesignerAddSocket(div, decl, self, socket);
			for (var senderID in rconnections){
				var sender = rconnections[senderID];
				var ctrl = sender.ctrls[name];
				ctrl[socket].addOutput(sockets[socket]);
			}
		}
	}
}

var ObjectSocket = function(){
	Socket.call(this, null, undefined, false);
	/*var oldAdd = this.addOutput;
	var self = this;
	this.addOutput = function(ctrl){
		oldAdd(ctrl);
		ctrl.connectReverse(self.get());//TODO: in case self point to null?
	}
	var oldRemove = this.removeOutput;
	this.removeOutput = function(ctrl){
		oldRemove(ctrl);
		ctrl.disconnectReverse(self.get());
	}//*/
}

CtrlDesignerAddSocket = function(div, decl, impl, socket){
	if (decl[socket] in _scalarCtrl){
		var scalar = ScalarCtrlDesigner(decl[socket], impl.sockets[socket], socket);
		div.appendChild(scalar);
	}else{//chip
		var idiv = document.createElement('div');
		div.appendChild(idiv);
		idiv.className = 'chipdiv';
		idiv.innerHTML = '<span>'+socket+'</span>';

		for (var chipS in decl[socket]){
			var scalar = ScalarCtrlDesigner(decl[socket][chipS], impl.sockets[socket][chipS], chipS);
			idiv.appendChild(scalar);
		}
	}
}

CtrlDesigner = function(name, decl, impl, activeSockets){
	var div = document.createElement('div');
	div.className = 'ctrldiv';
	div.innerHTML = '<span>'+name+'</span>';;
	_plate.appendChild(div);

	var closeBtn = document.createElement('input');
	closeBtn.type='button';
	closeBtn.value = 'X';
	closeBtn.className = 'closeBtn';
	div.appendChild(closeBtn);
	closeBtn.addEventListener('click', function(e){
		_plate.removeChild(div);
		impl.destroy();
	},false);

	var addBtn = document.createElement('input');
	addBtn.type='button';
	addBtn.value = '+';
	addBtn.className = 'closeBtn';
	div.appendChild(addBtn);
	addBtn.addEventListener('click', function(e){
		var config = {};
		for (var socket in decl){
			if (!(socket in impl.sockets)){
				config['Add '+socket] = function(s){
					return function(){
						impl.addSocket(s);
					}
				}(socket);
			}
		}
		var menu;
		config['Done'] = function(){
			_plate.removeChild(menu);
		}
		menu = Menu(config);
	},false);

	for (var key in activeSockets){
		CtrlDesignerAddSocket(div, decl, impl, activeSockets[key]);
	}

	return div;
}

var SocketGroup = function(name){
	var listeners = {};
	this._type = name;
	this.addListener = function(listener){
		listeners[listener._id] = listener;
	}
	this.removeListener = function(listener){
		delete listeners[listener._id];
	}

	var objs = {};
	this.add = function(val){
		objs[val._id] = val;
		for (var key in listeners){
			listeners[key].onAdd(val);
		}
	}
	this.remove = function(val){
		for (var key in listeners){
			listeners[key].onRemove(val);
		}
		delete objs[val._id];
	}
	this.getObjects = function(){
		return objs;
	}

	var self = this;
	this.addPlate = function(func){
		var listener = new IdObj();
		var cleanups= {};
		//current object
		var onAdd = function(obj){
			cleanups[obj._id] = func.call(obj);
		}
		for (var key in objs){
			onAdd(objs[key]);
		}
		//future object
		listener.onAdd = onAdd;
		listener.onRemove = function(obj){
			cleanups[obj._id].call(obj);
		}
		self.addListener(listener);
	}

	if (name in _scalarCtrl){
		this.set = function(val){//only for scalar
			for (var obj in objs){
				obj.set(val);
			}
		}
	}
}

//permanent connection
var SocketGroupImpl = function(group, activeSockets){
	IdObj.call(this);
	var self = this;
	self.sockets = {};
	activeSockets.forEach(function(socket){
		self.sockets[socket] = {};
	});
	if ('Count' in self.sockets){
		var out = new _scalarCtrl['Number']();
		self.sockets['Count'] = out;
		//current objects
		var count = 0;//because this socket is created at the beginning?
		var objs = group.getObjects();
		for (var k in objs) count++;
		out.set(count);

		//future objects
		var listener = new IdObj();
		listener.onAdd = function(val){
			count++;
			out.set(count);
		}
		listener.onRemove = function(val){
			count--;
			out.set(count);
		}
		group.addListener(listener);
		self.addDest(function(){
			group.removeListener(listener);
		});
	}
	//TODO: make sockets for CtrlGroupImpl: 'first', 'last', 'random', 'max', 'min'
	if ('CJoin' in self.sockets){
		var singleIn = new Socket(null);//this single object should not be already in the group
		var boolIn = new _scalarCtrl['Boolean']();
		var groupOut = new SocketGroup(self._type);
		self.sockets = {
			'In':singleIn,
			'Condition':boolIn,
			'Out':groupOut
		}
		//TODO: implement CJoin
	}


	var desi = new CtrlDesigner('abc', {'Count':'Number'}, self, activeSockets);
}

var CtrlGroupDesigner = function(impl){
	var div = document.createElement('div');
	div.className = 'slotdiv';

	div.innerHTML = 'group';
	return div;
}


var AdditiveSwitch = function(srcObj, groupCtrl){
	IdObj.call(this);
	this.set = function(bool){
		if(bool)
			groupCtrl.add(srcObj);
		else
			groupCtrl.remove(srcObj);
	}
}

var _scalarCtrl = {
	'Number':function(){
		Socket.call(this,0);
	},
	'String':function(){
		Socket.call(this,'');
	},
	'Boolean':function(){
		Socket.call(this,false);
	},
	'Signal':function(){
		SignalSocket.call(this);
	}
};

(function(){
	var broadcasts = {};
	BroadcastImpl = function(name){//one per name!
		CtrlGroupImpl.call(this);
		broadcasts[name] = this;
	}
	_getBroadcast = function(name){
		return broadcasts[name];
	}
})();

/** @constructor */
var NumberCtrlImpl = function(){//handle on-demand sockets
	if ('sqr' in this.sockets){
		var sqrOut = this.sockets['sqr'];
		var handler = new IdObj();
		handler.set = function(val){
			sqrOut.set(val*val);
		}
		this.addOutput(handler);
	}
	if ('plus' in this.sockets){
		var plusIn = this;
		var another = new _scalarCtrl['Number']();
		var out = new _scalarCtrl['Number']();
		this.sockets['plus'] = {
			'another':another,
			'out':out
		};
		var handler = new IdObj();
		handler.set = function(){
			out.set(plusIn.get()+another.get());
		}
		this.addOutput(handler);
		another.addOutput(handler);
	}
	if ('greater' in this.sockets){
		var greaterIn = this;
		var another = new _scalarCtrl['Number']();
		var out = new _scalarCtrl['Boolean']();
		this.sockets['greater'] = {
			'another':another,
			'out':out
		};
		var handler = new IdObj();
		handler.set = function(){
			out.set(greaterIn.get() > another.get());
		}
		this.addOutput(handler);
		another.addOutput(handler);
	}
}

Menu = function(config){
	var menu = document.createElement('div');
	menu.className = 'contextMenu';
	for (var label in config){
		var f = config[label];
		var btn = document.createElement('input');
		btn.type = 'button';
		btn.value = label;
		(function(func){
			btn.addEventListener('click', func, false);
		})(f);
		menu.appendChild(btn);
	}
	_plate.appendChild(menu);//TODO: make the context menu appear correctly
	return menu;
}

ScalarCtrlDesigner = function(name, impl, caption){
	var div = document.createElement('div');
	div.className = 'slotdiv';

	var captionE = document.createElement('span');
	captionE.className = 'slotcaption';
	captionE.innerHTML = caption;
	var config = {
		'Delete':function(){
			_plate.removeChild(menu);
			div.parentNode.removeChild(div);
			impl.destroy();
		}
	};
	if (name in _scalarCtrl){
		config['CreateCtrl'] = function(){
			var ctrl = new _ctrlImpl(name, []);
			impl.addOutput(ctrl);
		}
	}else{
		//*
		var decl = _getProtoDecl(name);
		decl.ctrls.forEach(function(ctrl){
			config['Ctrl_'+ctrl] = function(){
				var ctrlImpl = new _ctrlImpl(ctrl, []);
				impl.addOutput(ctrlImpl);
			}
		});//*/
	}
	captionE.addEventListener('click', function(e){
		var menu;
		config['Done'] = function(){
			_plate.removeChild(menu);
		}
		menu = Menu(config);
	},false);
	div.appendChild(captionE);

	var elem = document.createElement('input');
	elem.className = 'slotvalue';
	div.appendChild(elem);

	if (name=='Signal'){
		elem.type = 'button';
		elem.value = 'signify';
		elem.addEventListener('click', function(e){
			impl.set();
		},false);
	}else if (name in _scalarCtrl){
		//elem.type = 'number';
		var doNotTrigger = false;
		var handler = new IdObj();
		handler.set = function(val){
			if (doNotTrigger){
				doNotTrigger = false;
				return;
			}
			elem.value = JSON.stringify(val);
		};
		impl.addOutput(handler);
		elem.addEventListener('change',function(e){
			doNotTrigger = true;
			impl.set(JSON.parse(elem.value));
		},false);
	}else{//normal proto

	}
	return div;
}

_registerCtrl('Number',{
	'sqr':'Number',
	'plus':{
		'another':'Number',
		'out':'Number'
	}
}, NumberCtrlImpl);



//parsing
//e.g. function(){ return id}//no semicolon!
var _getReturnCode = function(func){
	var fstr = func.toString();
	//console.log(fstr);
	var ret_stat = fstr.indexOf('return');
	var ret_end = fstr.indexOf('}');
	return fstr.substring(ret_stat + 'return'.length, ret_end).trim();
}

//e.g. function(a){return a.prop}//no semicolon!
var _getPropCode = function(func){
	var fstr = func.toString();
	//console.log(fstr);
	var ret_stat = fstr.indexOf('.');
	var ret_end = fstr.indexOf('}');
	return fstr.substring(ret_stat+1, ret_end).trim();
}

var _parseOPath = function(path){
	var ret = path[0];
	var _socketsProp = _getPropCode(function(a){
		return a.sockets
	});
	if (path.length>1) ret += '.' + _socketsProp;
	for (var i=1;i<path.length; i++){
		ret += '.' + path[i]
	}
	return ret;
}

var _parseProtoDecl = function(decl){
	var ret = '';
	var _createId = _getReturnCode(function(){
		return _create
	});
	var _ctrlsProp = _getPropCode(function(a){
		return a.ctrls
	});
	var _setProp = _getPropCode(function(a){
		return a.set
	});
	//objs
	for (var obj in decl.objs){
		var dobj = decl.objs[obj];
		//constructor
		var _createSrc = 'var '+obj+' = '
			+ _createId+'('+ JSON.stringify(dobj.proto) + ', '
			+ JSON.stringify(dobj.cons) + ');\n';
		ret+= _createSrc;

		//adjusments
		for (var ctrl in dobj.adjustments){
			var cobj = dobj.adjustments[ctrl];
			for (var socket in cobj){
				var adj = obj + '.' +_ctrlsProp
					+'.' + ctrl
					+'.' + socket + '.'+_setProp
					+'(' + JSON.stringify(cobj[socket]) + ');\n';
				ret+=adj;
			}
		}
	}

	//ctrls
	var _ctrlImplId = _getReturnCode(function(){
		return _ctrlImpl
	});
	for (var ctrl in decl.ctrls){//create
		var cobj = decl.ctrls[ctrl];
		var cons = [];
		for (var activeSocket in cobj.sockets){
			cons.push(activeSocket);
		}
		var src = 'var '+ctrl+' = '+_ctrlImplId+'('
			+JSON.stringify(cobj.ctrl) + ', '
			+JSON.stringify(cons)+');\n';
		ret+=src;
	}
	var _connectProp = _getPropCode(function(a){
		return a.connect
	});
	var _socketsProp = _getPropCode(function(a){
		return a.sockets
	});
	var _addOutputProp = _getPropCode(function(a){
		return a.addOutput
	});
	for (var ctrl in decl.ctrls){//connections
		var cobj = decl.ctrls[ctrl];
		if (cobj.outputs){
			cobj.outputs.forEach(function(output){
				var src = ctrl + '.' + _connectProp
					+ '(' + _parseOPath(output) + ');\n';
				ret += src;
			});
		}
		for (var socket in cobj.sockets){
			var osocket = cobj.sockets[socket];
			if (osocket instanceof Array){//socket
				osocket.forEach(function(path){
					var src = ctrl + '.' + _socketsProp
						+ '.' + socket
						+ '.' + _addOutputProp
						+ '(' + _parseOPath(path) + ');\n';
					ret += src;
				});
			}else{//chip
				for (var chipSocket in osocket){
					var ocsocket = osocket[chipSocket];
					ocsocket.forEach(function(path){
						var src = ctrl + '.' + _socketsProp
							+ '.' + socket
							+ '.' + chipSocket
							+ '.' + _addOutputProp
							+ '(' + _parseOPath(path) + ');\n';
						ret += src;
					});
				}
			}
		}
	}
	//console.log(ret);
	return new Function(ret);
}

var _aaa = function(){
	var protos = {};
	var proto_decls = {};
	_registerProto = function(name, cons, decl){
		protos[name] = cons;
		proto_decls[name] = decl;
	}
	_getProtoDecl = function(name){
		return proto_decls[name];
	}
	var objs = {};
	_object = function(proto, params){
		var pro = protos[proto];
		var obj = Object.create(pro.prototype);
		pro.apply(obj, params);
		obj._class = proto;//TODO: where to store _class variable?
		objs[obj._id] = obj;
		return obj;
	}
	var removeObj = function(obj){
		obj.destroy();
		delete objs[obj._id];
	}

	//reference to a powerPlate (similar to a function call)
	var PowerPlateImpl = function(){
		IdObj.call(this);
		this.insignal = new _scalarCtrl['Signal']();
		//this.outsignal = new _scalarCtrl['Signal']();
	}

	//create an instance of a proto
	ProtoImpl = function(name, single_out){
		PowerPlateImpl.call(this);
		var declParams = proto_decls[name].params;
		var sockets = {};
		this.sockets = sockets;
		var activeSockets = [];
		for (var socket in declParams){
			sockets[socket] = new _scalarCtrl[declParams[socket]]();
			activeSockets.push(socket);
		}
		var div = CtrlDesigner(name, declParams, this, activeSockets);
		var indesigner = ScalarCtrlDesigner('Signal', this.insignal, 'create');
		div.appendChild(indesigner);

		var self = this;
		var inset = function(){
			var params = [];
			for (var socket in declParams){
				params.push(sockets[socket].get());
			}
			return _object(name, params);
		}

		if (single_out){
			this.out = new ObjectSocket();
			this.insignal.set = function(){
				var obj = inset();
				self.out.set(obj);
			}
			var outDesi = ScalarCtrlDesigner(name, this.out, 'object');
			div.appendChild(outDesi);
		}
		else{
			this.insignal.set = function(){
				var obj = inset();
				self.out.add(obj);
			}
			this.out = new SocketGroup(name);//TODO: make a special Object Holder socket, which can connect to ctrl!
		}

		//var outDesi = new CtrlGroupDesigner(this.outs);
		//div.appendChild(outDesi);
	}

	ObjectCtrl = function(obj){
		var signal = new _scalarCtrl['Signal']();
		signal.set = function(){
			removeObj(obj);
		}
		this['Destroy'] = signal;
		this.destroy = function(){
			signal.destroy();
		}
	}
	_registerCtrl('Object',{
		'Destroy':'Signal'
	},function(){});
};
_aaa();

var SetControlMode;
(function(){
	var curMode;
	SetControlMode = function(control){
		if (curMode) curMode.uninstall();
		curMode = control;
		curMode.install();
	}
})();
